/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package copecscheduling;

import java.util.ArrayList;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import java.util.Random;
import java.util.Arrays;
import java.util.Date;
import java.util.regex.*;
import java.math.*;

/**
 *
 * @author nacho
 */

public class Schedule extends Thread {

    private static final int NROBOMBEROS=13;
    private static final int NROGEN=0;
    private static final int MAXGEN = 50000;
    private static final int MAXRESTART = 50;
    private static final int WEEKLIMIT = 2700;
    private static final int HISTORYDAYS = 7;
    private static final int EXPLORE = 1;
    private static final int EXPLOTE = 2;
    private static final int CROSSPOINTS = 5;
    private static final int NEWSCHEDULE = 1;
    private int current_gen = 0;
    private int current_restart = 0;
    private int num_days;
    private int shift1 = 4;
    private int shift2 = 4;
    private int shift3 = 2;
    private int mut = 0;
    private int scape = 0;
    private int gen = 0;

    private float max_mutation_freq = 0;
    private float min_mutation_freq = 1;
    private float max_crossover_freq = 1;
    private float min_crossover_freq = 0 ;
    private float max_escape_freq = 1;
    private float min_escape_freq = 0;

    private Random rand = new Random();

    private Bombero temp = new Bombero();
    private OutputHelper helper;
    
    private ArrayList<Integer> days = new ArrayList();
    private ArrayList<Integer> sundays = new ArrayList();
    private ArrayList<Integer> history_days = new ArrayList();
    private ArrayList<Integer> history_sundays = new ArrayList();
    private ArrayList<Bombero> bestSchedule;
    private ArrayList<Bombero> bomberos;

    private String dayNames[] = new DateFormatSymbols().getWeekdays();
    private Calendar date = Calendar.getInstance();

    
    public Schedule(){
        
    }

    public void run(){
        int freq;
        this.num_days = 30;
        this.helper = new OutputHelper();
        this.init();

        greedy();
        setDays();

        for(Bombero b1:this.bomberos){
            evaluateFitness(b1);
        }

        copyBestGen();

        System.out.println();
        System.out.println(getTotalFitness(bomberos));
        helper.printSchedule(bomberos);

        //helper.printBombero(this.bomberos.get(0), this.bomberos.indexOf(this.bomberos.get(0)));

        do{
            do{

            freq = rand.nextInt(3);


            if(freq == 0)mutation();
            else if(freq == 1)generation();
            else if(freq == 2)escape();


            }while(current_gen < MAXGEN && getTotalFitness(bomberos) != 0);

            if(getTotalFitness(bomberos) == 0) break;

            copyBestGen(NEWSCHEDULE);

            current_gen = 0;
            current_restart ++;
            System.out.println("Restart: "+current_restart + " Fitness: "+ getTotalFitness(bestSchedule));

            if(current_restart % 25 == 0){
                System.out.println();
                System.out.println(getTotalFitness(bestSchedule));
                helper.printSchedule(bestSchedule);
            }

        }while(current_restart < MAXRESTART);

        System.out.println();
        System.out.println(getTotalFitness(bestSchedule));
        helper.printSchedule(bestSchedule);
        System.out.println("Mutation: "+this.mut);
        System.out.println("Scape: "+this.scape);
        System.out.println("Generation:"+this.gen);
        System.out.println("Current Gen:"+this.current_gen);

        if(shiftLimit(this.bestSchedule)) System.out.println("true");
        
    }


    private void init(){
        bomberos = new ArrayList<Bombero>();
        for(int i=0;i<NROBOMBEROS;i++){
            temp = new Bombero();
            temp.init(num_days,HISTORYDAYS,date);
            bomberos.add(temp);
            temp = null;
        }

        bestSchedule = new ArrayList<Bombero>();
        for(int i=0;i<NROBOMBEROS;i++){
            temp = new Bombero();
            temp.init(num_days,HISTORYDAYS,date);
            bestSchedule.add(temp);
            temp = null;
        }
    }

    private void greedy(){
        int index;
        int shift=1;
        int count=0;
        ArrayList<Integer> list = new ArrayList<Integer>();
        for(int i=0;i<num_days;i++){
            list.clear();
            initList(list);
            //helper.printList(list);
            for(int j=0;j<NROBOMBEROS;j++){
                index = rand.nextInt(NROBOMBEROS);
                while(checkList(list,index)){
                    index = rand.nextInt(NROBOMBEROS);
                }
                this.bomberos.get(index).getPlanification().set(i, shift);
                list.set(j, index);
                count++;
                if(count == shift1 && (shift == 1 || shift == 2)){
                    shift++;
                    count = 0;
                }
                if(count == shift3 && shift == 3){
                    shift=1;
                    count = 0;
                    break;
                }
            }
        }
    }
    
    private void initList(ArrayList<Integer> l){
        for(int i=0;i<NROBOMBEROS;i++){
            l.add(-1);
        }
    }

    private boolean checkList(ArrayList<Integer> l,int index){
        for(int i=0;i<l.size();i++){
            if(l.get(i) == index){
                return true;
            }
        }
        return false;
    }

    private int[] parentIndex(ArrayList<Bombero> bomberos){
        float fitness;
        int index[] = new int[2];
        int i;
        boolean flag = false;
        Random local_rand = new Random();
        Date time = new Date();

        local_rand.setSeed(time.getTime());

        fitness = bomberos.get(0).getFitness();

        for(i=0;i<Schedule.NROBOMBEROS;i++){
            if(fitness < bomberos.get(i).getFitness()){
                index[0] = i;
                fitness = bomberos.get(i).getFitness();
            }
        }
 
        while(flag){
            index[1] = local_rand.nextInt(Schedule.NROBOMBEROS);
            if(index[1] != index[0]) flag = false;
        }

        return index;
    }

    private ArrayList<Integer> generateCrossPoints(int num_points){
        int tmp;
        boolean flag = true;
        ArrayList<Integer> cross_points = new ArrayList();
        Random local_rand = new Random();
        Date time = new Date();

        local_rand.setSeed(time.getTime());


        while(flag){
            tmp = local_rand.nextInt(this.num_days);

            for(int points : cross_points){
                if(points == tmp) flag = false;
            }

            if(flag)
                cross_points.add(tmp);
            
            if(cross_points.size() < num_points) flag = true;
            else flag = false;

        }

        return cross_points;
    }

    private void generation(){
        int i;
        int parent_index[];
        int cp1,cp2;
        int first_crossover = 9;
        Random local_rand = new Random();
        Date time = new Date();

        local_rand.setSeed(time.getTime());

        parent_index = parentIndex(bomberos);

        if(getTotalFitness(bomberos) <= 20) first_crossover = 3;
        else first_crossover = 8;
  
        if(local_rand.nextInt(10) <= first_crossover){
            for(cp1=0;cp1<this.num_days;cp1++){
                for(cp2=cp1+1;cp2<this.num_days;cp2++){
                    crossover(bomberos.get(parent_index[0]),bomberos.get(parent_index[1]),cp1,cp2);

                    evaluateFitness(bomberos.get(parent_index[0]));
                    evaluateFitness(bomberos.get(parent_index[1]));

                    if(shiftLimit(this.bomberos)){
                            if(getTotalFitness(bomberos)<getTotalFitness(this.bestSchedule) /*&&
                            getStandardDev(bomberos)<getStandardDev(this.bestSchedule)*/){
                                copyBestGen();
                            }
                    }
                    this.current_gen++;
                    gen++;
                }
            }
        }

        else{
            for(i=0;i<this.sundays.size();i++){
                cp1 = this.sundays.get(i);

                crossover(bomberos.get(parent_index[0]),bomberos.get(parent_index[1]),cp1);

                evaluateFitness(bomberos.get(parent_index[0]));
                evaluateFitness(bomberos.get(parent_index[1]));

                if(shiftLimit(this.bomberos)){
                        if(getTotalFitness(bomberos)<getTotalFitness(this.bestSchedule) /*&&
                        getStandardDev(bomberos)<getStandardDev(this.bestSchedule)*/){
                            copyBestGen();
                        }
                }
            }
            this.current_gen++;
            gen++;
        }
        
        
    }

    private void crossover(Bombero b1,Bombero b2, int cross_points){
        boolean flag = true;
        int i;
        int tmp;
        int ratio = 0;
       
        for(i=0;i<this.num_days;i++){
            if(flag){
                if(i == cross_points - ratio - 1){
                    tmp = b1.getPlanification().get(i);
                    b1.getPlanification().set(i, b2.getPlanification().get(i));
                    b2.getPlanification().set(i,tmp);
                    ratio --;
                }
                
                if(ratio < 0){
                    ratio = 1;
                    flag = false;
                }
            }
            else{
                if(i == cross_points + ratio - 1){
                    tmp = b1.getPlanification().get(i);
                    b1.getPlanification().set(i, b2.getPlanification().get(i));
                    b2.getPlanification().set(i,tmp);
                    ratio ++;
                }
            }
        }

//        for(int point: cross_points){
//            tmp = b1.getPlanification().get(point);
//            b1.getPlanification().set(point, b2.getPlanification().get(point));
//            b2.getPlanification().set(point,tmp);
//        }

    }

    private void crossover(Bombero b1, Bombero b2, int cp1,int cp2){
        int i;
        int tmp;

        for(i=0;i<this.num_days;i++){
            if(i >= cp1 && i < cp2){
                tmp = b1.getPlanification().get(i);
                b1.getPlanification().set(i, b2.getPlanification().get(i));
                b2.getPlanification().set(i,tmp);
            }
        }
    }

    private int select(int nro){
        return rand.nextInt(nro);
    }

    private void mutation(){
//        int mp1;
//        int mp2;
//        int b1;
//        int b2;
//        int aux;
//
//        b1 = select(NROBOMBEROS);
//        b2 = select(NROBOMBEROS);
//
//        mp1 = select(num_days);
//        mp2 = select(num_days);
//
//
//        aux = this.bomberos.get(b1).getPlanification().get(mp1);
//        this.bomberos.get(b1).getPlanification().set(mp1, this.bomberos.get(b2).getPlanification().get(mp2));
//        this.bomberos.get(b2).getPlanification().set(mp2, aux);
//
//        evaluateFitness(this.bomberos.get(b1));
//        evaluateFitness(this.bomberos.get(b2));
//
//        if(shiftLimit(this.bomberos)){
//            if(getTotalFitness(bomberos)<getTotalFitness(this.bestSchedule) &&
//                    getStandardDev(bomberos)<getStandardDev(this.bestSchedule)){
//                        copyBestGen();
//                        mut++;
//                }
//            setConfig(EXPLOTE);
//        }
//
//        this.current_gen++;

          //--------- Nueva mutación ---------//
          ArrayList<Integer> cross_points;
          float fitness[] = new float[Schedule.NROBOMBEROS];
          float worst_best[] = new float[4];
          int index[] = new int[4];
          int i;
          int j;
          int tmp;


          for(i=0;i<Schedule.NROBOMBEROS;i++){
              fitness[i] = this.bomberos.get(i).getFitness();
          }

          Arrays.sort(fitness);


          for(i=0;i<4;i++){
              worst_best[i] = fitness[NROBOMBEROS-i-1];
          }

          for(i=0;i<4;i++){
                for(j=0;j<NROBOMBEROS;j++){
                    if(worst_best[i] == bomberos.get(j).getFitness()){
                        index[i] = j;
                    }
                }
          }

          cross_points = generateCrossPoints(CROSSPOINTS);

          for(i=0;i<CROSSPOINTS;i++){
              tmp = bomberos.get(index[0]).getPlanification().get(cross_points.get(i));
              bomberos.get(index[0]).getPlanification().set(cross_points.get(i), bomberos.get(index[3]).getPlanification().get(cross_points.get(i)));
              bomberos.get(index[3]).getPlanification().set(cross_points.get(i), tmp);
          }

          for(i=0;i<CROSSPOINTS;i++){
              tmp = bomberos.get(index[1]).getPlanification().get(cross_points.get(i));
              bomberos.get(index[1]).getPlanification().set(cross_points.get(i), bomberos.get(index[2]).getPlanification().get(cross_points.get(i)));
              bomberos.get(index[2]).getPlanification().set(cross_points.get(i), tmp);
          }

        for(i=0;i<4;i++)
            evaluateFitness(this.bomberos.get(index[i]));

        if(shiftLimit(this.bomberos)){
            if(getTotalFitness(bomberos)<getTotalFitness(this.bestSchedule) /*&&
                    getStandardDev(bomberos)<getStandardDev(this.bestSchedule)*/){
                        copyBestGen();
                        
                }
            setConfig(EXPLOTE);
        }
        mut++;
        this.current_gen++;
    }

    private void escape(){
        int b1;
        int b2;
        int ep1;
        int ep2;
        int aux;

        b1 = select(NROBOMBEROS);
        b2 = select(NROBOMBEROS);

        ep1 = select(num_days);
        ep2 = select(num_days);

        while(ep1 == ep2){
            ep2 = select(num_days);
        }
        for(int i=0;i<num_days;i++){
            if(i>=ep1 && i<ep2){
               aux = this.bomberos.get(b1).getPlanification().get(i);
               this.bomberos.get(b1).getPlanification().set(i, this.bomberos.get(b2).getPlanification().get(i));
               this.bomberos.get(b2).getPlanification().set(i, aux);
            }
        }
        
        evaluateFitness(this.bomberos.get(b1));
        evaluateFitness(this.bomberos.get(b2));

        if(shiftLimit(this.bomberos)){
            if(getTotalFitness(bomberos)<getTotalFitness(this.bestSchedule) /*&&
                    getStandardDev(bomberos)<getStandardDev(this.bestSchedule)*/){
                        copyBestGen();
                }
        }
        scape++;
    }

    private void evaluateFitness(Bombero b1){
        b1.setFitness(0);
        weekLimit(b1);
        dayLimit(b1);
        dayOff(b1);
        //patterns(b1);
    }

    private void setDays(){
        int i;

        date.setFirstDayOfWeek(Calendar.MONDAY);
        date.set(Calendar.DATE,1);


//      Trabajando solo con septiembre...30 dias.
        date.set(Calendar.MONTH,Calendar.SEPTEMBER);
        this.num_days = date.getActualMaximum(Calendar.DAY_OF_MONTH);

        for(i=0;i<this.num_days;i++){

            this.days.add(date.get(Calendar.DATE));
            
            if(dayNames[date.get(Calendar.DAY_OF_WEEK)].equals("domingo")){
                this.sundays.add(date.get(Calendar.DATE));
                
            }

            date.roll(Calendar.DATE, true);

        }

        date.roll(Calendar.MONTH, false);
        if(date.get(Calendar.MONDAY) == 11) date.roll(Calendar.YEAR, false);

        date.set(Calendar.DATE,date.getActualMaximum(Calendar.DAY_OF_MONTH)- HISTORYDAYS);

        for(i=date.getActualMaximum(Calendar.DAY_OF_MONTH)- HISTORYDAYS;i> date.getActualMaximum(Calendar.DAY_OF_MONTH);i++){

            this.history_days.add(date.get(Calendar.DATE));

            if(dayNames[date.get(Calendar.DAY_OF_WEEK)].equals("domingo")){
                this.history_sundays.add(date.get(Calendar.DATE));
            }

            date.roll(Calendar.DATE, true);


        }


    }

    private void weekLimit(Bombero b1){
        float fitness;
        int weekHour = 0;
        int i;
        int j = 0;
       
        for(i=0;i<this.num_days + HISTORYDAYS ;i++){

//            if(i<7){
//                switch(b1.getHistory().get(i)){
//                    case 1: weekHour+= 450;
//                            break;
//                    case 2: weekHour+= 420;
//                            break;
//                    case 3: weekHour+= 420;
//                            break;
//                }
//
//                for(int sunday: this.history_sundays){
//                    if(sunday == this.history_days.get(i)){
//                        if(weekHour > WEEKLIMIT){
//                            fitness = b1.getFitness() + weekHour;
//                            b1.setFitness(fitness);
//                            weekHour = 0;
//                        }
//                    }
//                }
//
//            }

            //mientras no sea domingo, se suman las horas de los turnos semanales, si se llega a un domingo
            //se verifica si se paso o no de las 45 horas.
            if(i>=7){
                //System.out.println(weekHour);
                switch(b1.getPlanification().get(j)){
                    case 1: weekHour+= 450;
                            break;
                    case 2: weekHour+= 420;
                            break;
                    case 3: weekHour+= 420;
                            break;
                }


                for(int sunday: this.sundays){
                    if(sunday == this.days.get(j)){
                        //System.out.println("Domingo: "+ sunday);
                        if(weekHour > WEEKLIMIT){
                            fitness = b1.getFitness() + weekHour - WEEKLIMIT;
                            b1.setFitness(fitness);
                        }
                        weekHour = 0;

                    }
                }
                j++;
            }
        }
        
    }

    //Cuando se pase mas de 6 dias seguidos trabajando se aumenta la penalización en 200 por cada vez.
    private void dayLimit(Bombero b1){
        float fitness;
        int days_count = 0;
        int cases = 0;
        int i;

        for(i = 0;i < num_days;i++){
            if(b1.getPlanification().get(i) != 0){
                days_count++;
            }
            else days_count = 0;

            if(days_count > 6){
                days_count = 0;
                cases++;
            }
        }

        if(cases != 0){
            fitness = b1.getFitness() + cases;
            b1.setFitness(fitness);
        }

    }

    private void dayOff(Bombero b1){
        int dayOff = 0;
        float fitness = 0;

        for(int sunday: this.sundays){
            if(b1.getPlanification().get(sunday-1) == 0) dayOff ++;
        }

        switch(dayOff){
            case 0: fitness = 15;
                    break;
            case 1: fitness = 10;
                    break;
        }

        fitness = b1.getFitness() + fitness;

        b1.setFitness(fitness);


    }

    private void patterns(Bombero b1){
        Pattern pattern_1;
        Pattern pattern_2;
        Matcher match_1;
        Matcher match_2;

        pattern_1 = Pattern.compile("31");
        //pattern_2 = Pattern.compile("32");

        match_1 = pattern_1.matcher(b1.getStringPlanification());
        //match_2 = pattern_2.matcher(b1.getStringPlanification());

        if(match_1.find()){
            b1.setFitness(b1.getFitness() + 8);
        }
//        if(match_2.find()){
//            b1.setFitness(b1.getFitness() + 8);
//        }

        
//        if(b1.getStringPlanification().matches("31")){
//            System.out.println("prueba");
//            b1.setFitness(b1.getFitness() + 8);
//        }
//        if(b1.getStringPlanification().matches("31")){
//            System.out.println("prueba");
//            b1.setFitness(b1.getFitness() + 8);
//        }

    }

    private boolean shiftLimit(ArrayList<Bombero> bomberos){

        int index;
        int currentDay = 0;
        int s1 = 0;
        int s2 = 0;
        int s3 = 0;


        for(currentDay = 0 ; currentDay < this.num_days ; currentDay++){
            for(index = 0 ; index < Schedule.NROBOMBEROS ; index ++){

                if(bomberos.get(index).getPlanification().get(currentDay) == 1){
                    s1++;
                }
                else if(bomberos.get(index).getPlanification().get(currentDay) == 2){
                    s2++;
                }
                else if(bomberos.get(index).getPlanification().get(currentDay) == 3){
                    s3++;
                }
            }

            if(s1 != shift1 || s2 != shift2 || s3 != shift3) return false;
                
            s1 = 0;
            s2 = 0;
            s3 = 0;
        }
        
        return true;
    }

    private float getTotalFitness(ArrayList<Bombero> bomberos){
        float total = 0;
        int i;

        for(i = 0; i< NROBOMBEROS ; i++){
            total += bomberos.get(i).getFitness();
        }

        total = total/Schedule.NROBOMBEROS;

        return total;
    }

    private double getStandardDev(ArrayList<Bombero> bomberos){
        int i;
        double sd = 0;
        double avg = 0;

        avg = getTotalFitness(bomberos)/Schedule.NROBOMBEROS;

        for(i=0;i<Schedule.NROBOMBEROS;i++){

            sd+= Math.pow(bomberos.get(i).getFitness() - avg,2);
        }

        sd+= Math.sqrt(sd/Schedule.NROBOMBEROS);


        return sd;
    }

    
    private void copyBestGen(){
        int i;

        for(i=0;i<NROBOMBEROS;i++){
            this.bestSchedule.get(i).setFitness(this.bomberos.get(i).getFitness());
            this.bestSchedule.get(i).setHistory(this.bomberos.get(i).getHistory());
            this.bestSchedule.get(i).setPlanification(this.bomberos.get(i).getPlanification());
        }
    }

    private void copyBestGen(int newSchedule){
        int i;

        for(i=0;i<NROBOMBEROS;i++){
            this.bomberos.get(i).setFitness(this.bestSchedule.get(i).getFitness());
            this.bomberos.get(i).setHistory(this.bestSchedule.get(i).getHistory());
            this.bomberos.get(i).setPlanification(this.bestSchedule.get(i).getPlanification());
        }
    }



    private void setConfig(int option){

        if(option == 1){
            this.min_mutation_freq = 0;
            this.max_mutation_freq = 1;

            this.min_crossover_freq = -1;
            this.max_crossover_freq = -1;

            this.min_escape_freq = -1;
            this.max_escape_freq = -1;
        }

        else if(option == 2){
            this.min_mutation_freq = 0;
            this.max_mutation_freq = (float)0.2;

            this.min_crossover_freq = (float)0.2;
            this.max_crossover_freq = (float)0.9;

            this.min_escape_freq = (float)0.9;
            this.min_escape_freq = 1;
        }
    }
}

